home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2009 March / ME_03_2009.iso / Software / Internet / Firefox 3.0.8.dmg / Firefox.app / Contents / MacOS / modules / utils.js < prev    next >
Encoding:
JavaScript  |  2009-03-26  |  59.4 KB  |  1,680 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is the Places Command Controller.
  16.  *
  17.  * The Initial Developer of the Original Code is Google Inc.
  18.  * Portions created by the Initial Developer are Copyright (C) 2005
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Myk Melez <myk@mozilla.org>
  24.  *   Asaf Romano <mano@mozilla.com>
  25.  *   Sungjoon Steve Won <stevewon@gmail.com>
  26.  *   Dietrich Ayala <dietrich@mozilla.com>
  27.  *
  28.  * Alternatively, the contents of this file may be used under the terms of
  29.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  30.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31.  * in which case the provisions of the GPL or the LGPL are applicable instead
  32.  * of those above. If you wish to allow use of your version of this file only
  33.  * under the terms of either the GPL or the LGPL, and not to allow others to
  34.  * use your version of this file under the terms of the MPL, indicate your
  35.  * decision by deleting the provisions above and replace them with the notice
  36.  * and other provisions required by the GPL or the LGPL. If you do not delete
  37.  * the provisions above, a recipient may use your version of this file under
  38.  * the terms of any one of the MPL, the GPL or the LGPL.
  39.  *
  40.  * ***** END LICENSE BLOCK ***** */
  41.  
  42. function LOG(str) {
  43.   dump("*** " + str + "\n");
  44. }
  45.  
  46. var EXPORTED_SYMBOLS = ["PlacesUtils"];
  47.  
  48. var Ci = Components.interfaces;
  49. var Cc = Components.classes;
  50. var Cr = Components.results;
  51.  
  52. const POST_DATA_ANNO = "bookmarkProperties/POSTData";
  53. const READ_ONLY_ANNO = "placesInternal/READ_ONLY";
  54. const LMANNO_FEEDURI = "livemark/feedURI";
  55. const LMANNO_SITEURI = "livemark/siteURI";
  56.  
  57. //@line 58 "/builds/tinderbox/Fx-Mozilla1.9-Release/Darwin_8.8.4_Depend/mozilla/toolkit/components/places/src/utils.js"
  58. // On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we
  59. // really just want "\n".
  60. const NEWLINE= "\n";
  61. //@line 65 "/builds/tinderbox/Fx-Mozilla1.9-Release/Darwin_8.8.4_Depend/mozilla/toolkit/components/places/src/utils.js"
  62.  
  63. function QI_node(aNode, aIID) {
  64.   var result = null;
  65.   try {
  66.     result = aNode.QueryInterface(aIID);
  67.   }
  68.   catch (e) {
  69.   }
  70.   return result;
  71. }
  72. function asVisit(aNode)    { return QI_node(aNode, Ci.nsINavHistoryVisitResultNode);    }
  73. function asFullVisit(aNode){ return QI_node(aNode, Ci.nsINavHistoryFullVisitResultNode);}
  74. function asContainer(aNode){ return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);}
  75. function asQuery(aNode)    { return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);    }
  76.  
  77. var PlacesUtils = {
  78.   // Place entries that are containers, e.g. bookmark folders or queries.
  79.   TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
  80.   // Place entries that are bookmark separators.
  81.   TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
  82.   // Place entries that are not containers or separators
  83.   TYPE_X_MOZ_PLACE: "text/x-moz-place",
  84.   // Place entries in shortcut url format (url\ntitle)
  85.   TYPE_X_MOZ_URL: "text/x-moz-url",
  86.   // Place entries formatted as HTML anchors
  87.   TYPE_HTML: "text/html",
  88.   // Place entries as raw URL text
  89.   TYPE_UNICODE: "text/unicode",
  90.  
  91.   /**
  92.    * The Bookmarks Service.
  93.    */
  94.   get bookmarks() {
  95.     delete this.bookmarks;
  96.     return this.bookmarks = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  97.                             getService(Ci.nsINavBookmarksService);
  98.   },
  99.  
  100.   /**
  101.    * The Nav History Service.
  102.    */
  103.   get history() {
  104.     delete this.history;
  105.     return this.history = Cc["@mozilla.org/browser/nav-history-service;1"].
  106.                           getService(Ci.nsINavHistoryService);
  107.   },
  108.  
  109.   /**
  110.    * The Live Bookmark Service.
  111.    */
  112.   get livemarks() {
  113.     delete this.livemarks;
  114.     return this.livemarks = Cc["@mozilla.org/browser/livemark-service;2"].
  115.                             getService(Ci.nsILivemarkService);
  116.   },
  117.  
  118.   /**
  119.    * The Annotations Service.
  120.    */
  121.   get annotations() {
  122.     delete this.annotations;
  123.     return this.annotations = Cc["@mozilla.org/browser/annotation-service;1"].
  124.                               getService(Ci.nsIAnnotationService);
  125.   },
  126.  
  127.   /**
  128.    * The Favicons Service
  129.    */
  130.   get favicons() {
  131.     delete this.favicons;
  132.     return this.favicons = Cc["@mozilla.org/browser/favicon-service;1"].
  133.                            getService(Ci.nsIFaviconService);
  134.   },
  135.  
  136.   /**
  137.    * The Places Tagging Service
  138.    */
  139.   get tagging() {
  140.     delete this.tagging;
  141.     return this.tagging = Cc["@mozilla.org/browser/tagging-service;1"].
  142.                           getService(Ci.nsITaggingService);
  143.   },
  144.  
  145.   /**
  146.    * Makes a URI from a spec.
  147.    * @param   aSpec
  148.    *          The string spec of the URI
  149.    * @returns A URI object for the spec.
  150.    */
  151.   _uri: function PU__uri(aSpec) {
  152.     return Cc["@mozilla.org/network/io-service;1"].
  153.            getService(Ci.nsIIOService).
  154.            newURI(aSpec, null, null);
  155.   },
  156.  
  157.   /**
  158.    * String bundle helpers
  159.    */
  160.   get _bundle() {
  161.     const PLACES_STRING_BUNDLE_URI =
  162.         "chrome://places/locale/places.properties";
  163.     delete this._bundle;
  164.     return this._bundle = Cc["@mozilla.org/intl/stringbundle;1"].
  165.                           getService(Ci.nsIStringBundleService).
  166.                           createBundle(PLACES_STRING_BUNDLE_URI);
  167.   },
  168.  
  169.   getFormattedString: function PU_getFormattedString(key, params) {
  170.     return this._bundle.formatStringFromName(key, params, params.length);
  171.   },
  172.  
  173.   getString: function PU_getString(key) {
  174.     return this._bundle.GetStringFromName(key);
  175.   },
  176.  
  177.   /**
  178.    * Determines whether or not a ResultNode is a Bookmark folder.
  179.    * @param   aNode
  180.    *          A result node
  181.    * @returns true if the node is a Bookmark folder, false otherwise
  182.    */
  183.   nodeIsFolder: function PU_nodeIsFolder(aNode) {
  184.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
  185.             aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
  186.   },
  187.  
  188.   /**
  189.    * Determines whether or not a ResultNode represents a bookmarked URI.
  190.    * @param   aNode
  191.    *          A result node
  192.    * @returns true if the node represents a bookmarked URI, false otherwise
  193.    */
  194.   nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
  195.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
  196.            aNode.itemId != -1;
  197.   },
  198.  
  199.   /**
  200.    * Determines whether or not a ResultNode is a Bookmark separator.
  201.    * @param   aNode
  202.    *          A result node
  203.    * @returns true if the node is a Bookmark separator, false otherwise
  204.    */
  205.   nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
  206.  
  207.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR);
  208.   },
  209.  
  210.   /**
  211.    * Determines whether or not a ResultNode is a visit item.
  212.    * @param   aNode
  213.    *          A result node
  214.    * @returns true if the node is a visit item, false otherwise
  215.    */
  216.   nodeIsVisit: function PU_nodeIsVisit(aNode) {
  217.     const NHRN = Ci.nsINavHistoryResultNode;
  218.     var type = aNode.type;
  219.     return type == NHRN.RESULT_TYPE_VISIT ||
  220.            type == NHRN.RESULT_TYPE_FULL_VISIT;
  221.   },
  222.  
  223.   /**
  224.    * Determines whether or not a ResultNode is a URL item.
  225.    * @param   aNode
  226.    *          A result node
  227.    * @returns true if the node is a URL item, false otherwise
  228.    */
  229.   uriTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
  230.              Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT,
  231.              Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT],
  232.   nodeIsURI: function PU_nodeIsURI(aNode) {
  233.     return this.uriTypes.indexOf(aNode.type) != -1;
  234.   },
  235.  
  236.   /**
  237.    * Determines whether or not a ResultNode is a Query item.
  238.    * @param   aNode
  239.    *          A result node
  240.    * @returns true if the node is a Query item, false otherwise
  241.    */
  242.   nodeIsQuery: function PU_nodeIsQuery(aNode) {
  243.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
  244.   },
  245.  
  246.   /**
  247.    * Determines if a node is read only (children cannot be inserted, sometimes
  248.    * they cannot be removed depending on the circumstance)
  249.    * @param   aNode
  250.    *          A result node
  251.    * @returns true if the node is readonly, false otherwise
  252.    */
  253.   nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
  254.     if (this.nodeIsFolder(aNode))
  255.       return this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  256.     if (this.nodeIsQuery(aNode) &&
  257.         asQuery(aNode).queryOptions.resultType !=
  258.           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
  259.       return aNode.childrenReadOnly;
  260.     return false;
  261.   },
  262.  
  263.   /**
  264.    * Determines whether or not a ResultNode is a host container.
  265.    * @param   aNode
  266.    *          A result node
  267.    * @returns true if the node is a host container, false otherwise
  268.    */
  269.   nodeIsHost: function PU_nodeIsHost(aNode) {
  270.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  271.            aNode.parent &&
  272.            asQuery(aNode.parent).queryOptions.resultType ==
  273.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
  274.   },
  275.  
  276.   /**
  277.    * Determines whether or not a ResultNode is a day container.
  278.    * @param   node
  279.    *          A NavHistoryResultNode
  280.    * @returns true if the node is a day container, false otherwise
  281.    */
  282.   nodeIsDay: function PU_nodeIsDay(aNode) {
  283.     var resultType;
  284.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  285.            aNode.parent &&
  286.            ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
  287.                Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  288.              resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
  289.   },
  290.  
  291.   /**
  292.    * Determines whether or not a result-node is a tag container.
  293.    * @param   aNode
  294.    *          A result-node
  295.    * @returns true if the node is a tag container, false otherwise
  296.    */
  297.   nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
  298.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  299.            asQuery(aNode).queryOptions.resultType ==
  300.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
  301.   },
  302.  
  303.   /**
  304.    * Determines whether or not a ResultNode is a container.
  305.    * @param   aNode
  306.    *          A result node
  307.    * @returns true if the node is a container item, false otherwise
  308.    */
  309.   containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
  310.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
  311.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
  312.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER],
  313.   nodeIsContainer: function PU_nodeIsContainer(aNode) {
  314.     return this.containerTypes.indexOf(aNode.type) != -1;
  315.   },
  316.  
  317.   /**
  318.    * Determines whether or not a ResultNode is an history related container.
  319.    * @param   node
  320.    *          A result node
  321.    * @returns true if the node is an history related container, false otherwise
  322.    */
  323.   nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
  324.     var resultType;
  325.     return this.nodeIsQuery(aNode) &&
  326.            ((resultType = asQuery(aNode).queryOptions.resultType) ==
  327.               Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
  328.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  329.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
  330.             this.nodeIsDay(aNode) ||
  331.             this.nodeIsHost(aNode));
  332.   },
  333.  
  334.   /**
  335.    * Determines whether or not a result-node is a dynamic-container item.
  336.    * The dynamic container result node type is for dynamically created
  337.    * containers (e.g. for the file browser service where you get your folders
  338.    * in bookmark menus).
  339.    * @param   aNode
  340.    *          A result node
  341.    * @returns true if the node is a dynamic container item, false otherwise
  342.    */
  343.   nodeIsDynamicContainer: function PU_nodeIsDynamicContainer(aNode) {
  344.     if (aNode.type == NHRN.RESULT_TYPE_DYNAMIC_CONTAINER)
  345.       return true;
  346.     return false;
  347.   },
  348.  
  349.  /**
  350.   * Determines whether a result node is a remote container registered by the
  351.   * livemark service.
  352.   * @param aNode
  353.   *        A result Node
  354.   * @returns true if the node is a livemark container item
  355.   */
  356.   nodeIsLivemarkContainer: function PU_nodeIsLivemarkContainer(aNode) {
  357.     // Use the annotations service directly to avoid instantiating
  358.     // the Livemark service on startup. (bug 398300)
  359.     return this.nodeIsFolder(aNode) &&
  360.            this.annotations.itemHasAnnotation(aNode.itemId, LMANNO_FEEDURI);
  361.   },
  362.  
  363.  /**
  364.   * Determines whether a result node is a live-bookmark item
  365.   * @param aNode
  366.   *        A result node
  367.   * @returns true if the node is a livemark container item
  368.   */
  369.   nodeIsLivemarkItem: function PU_nodeIsLivemarkItem(aNode) {
  370.     return aNode.parent && this.nodeIsLivemarkContainer(aNode.parent);
  371.   },
  372.  
  373.   /**
  374.    * Determines whether or not a node is a readonly folder.
  375.    * @param   aNode
  376.    *          The node to test.
  377.    * @returns true if the node is a readonly folder.
  378.   */
  379.   isReadonlyFolder: function(aNode) {
  380.     return this.nodeIsFolder(aNode) &&
  381.            this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  382.   },
  383.  
  384.   /**
  385.    * Gets the concrete item-id for the given node. Generally, this is just
  386.    * node.itemId, but for folder-shortcuts that's node.folderItemId.
  387.    */
  388.   getConcreteItemId: function PU_getConcreteItemId(aNode) {
  389.     if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
  390.       return asQuery(aNode).folderItemId;
  391.     else if (PlacesUtils.nodeIsTagQuery(aNode)) {
  392.       // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
  393.       // so we can still get the concrete itemId for them.
  394.       var queries = aNode.getQueries({});
  395.       var folders = queries[0].getFolders({});
  396.       return folders[0];
  397.     }
  398.     return aNode.itemId;
  399.   },
  400.  
  401.   /**
  402.    * Gets the index of a node within its parent container
  403.    * @param   aNode
  404.    *          The node to look up
  405.    * @returns The index of the node within its parent container, or -1 if the
  406.    *          node was not found or the node specified has no parent.
  407.    */
  408.   getIndexOfNode: function PU_getIndexOfNode(aNode) {
  409.     var parent = aNode.parent;
  410.     if (!parent)
  411.       return -1;
  412.     var wasOpen = parent.containerOpen;
  413.     var result, oldViewer;
  414.     if (!wasOpen) {
  415.       result = parent.parentResult;
  416.       oldViewer = result.viewer;
  417.       result.viewer = null;
  418.       parent.containerOpen = true;
  419.     }
  420.     var cc = parent.childCount;
  421.     for (var i = 0; i < cc && parent.getChild(i) != aNode; ++i);
  422.     if (!wasOpen) {
  423.       parent.containerOpen = false;
  424.       result.viewer = oldViewer;
  425.     }
  426.     return i < cc ? i : -1;
  427.   },
  428.  
  429.   /**
  430.    * String-wraps a result node according to the rules of the specified
  431.    * content type.
  432.    * @param   aNode
  433.    *          The Result node to wrap (serialize)
  434.    * @param   aType
  435.    *          The content type to serialize as
  436.    * @param   [optional] aOverrideURI
  437.    *          Used instead of the node's URI if provided.
  438.    *          This is useful for wrapping a container as TYPE_X_MOZ_URL,
  439.    *          TYPE_HTML or TYPE_UNICODE.
  440.    * @param   aForceCopy
  441.    *          Does a full copy, resolving folder shortcuts.
  442.    * @returns A string serialization of the node
  443.    */
  444.   wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI, aForceCopy) {
  445.     var self = this;
  446.  
  447.     // when wrapping a node, we want all the items, even if the original
  448.     // query options are excluding them.
  449.     // this can happen when copying from the left hand pane of the bookmarks
  450.     // organizer
  451.     function convertNode(cNode) {
  452.       if (self.nodeIsFolder(cNode) && asQuery(cNode).queryOptions.excludeItems) {
  453.         var concreteId = self.getConcreteItemId(cNode);
  454.         return self.getFolderContents(concreteId, false, true).root;
  455.       }
  456.       return cNode;
  457.     }
  458.  
  459.     switch (aType) {
  460.       case this.TYPE_X_MOZ_PLACE:
  461.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  462.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  463.         var writer = {
  464.           value: "",
  465.           write: function PU_wrapNode__write(aStr, aLen) {
  466.             this.value += aStr;
  467.           }
  468.         };
  469.         self.serializeNodeAsJSONToOutputStream(convertNode(aNode), writer, true, aForceCopy);
  470.         return writer.value;
  471.       case this.TYPE_X_MOZ_URL:
  472.         function gatherDataUrl(bNode) {
  473.           if (self.nodeIsLivemarkContainer(bNode)) {
  474.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  475.             return siteURI + NEWLINE + bNode.title;
  476.           }
  477.           if (self.nodeIsURI(bNode))
  478.             return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
  479.           // ignore containers and separators - items without valid URIs
  480.           return "";
  481.         }
  482.         return gatherDataUrl(convertNode(aNode));
  483.  
  484.       case this.TYPE_HTML:
  485.         function gatherDataHtml(bNode) {
  486.           function htmlEscape(s) {
  487.             s = s.replace(/&/g, "&");
  488.             s = s.replace(/>/g, ">");
  489.             s = s.replace(/</g, "<");
  490.             s = s.replace(/"/g, """);
  491.             s = s.replace(/'/g, "'");
  492.             return s;
  493.           }
  494.           // escape out potential HTML in the title
  495.           var escapedTitle = bNode.title ? htmlEscape(bNode.title) : "";
  496.           if (self.nodeIsLivemarkContainer(bNode)) {
  497.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  498.             return "<A HREF=\"" + siteURI + "\">" + escapedTitle + "</A>" + NEWLINE;
  499.           }
  500.           if (self.nodeIsContainer(bNode)) {
  501.             asContainer(bNode);
  502.             var wasOpen = bNode.containerOpen;
  503.             if (!wasOpen)
  504.               bNode.containerOpen = true;
  505.  
  506.             var childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
  507.             var cc = bNode.childCount;
  508.             for (var i = 0; i < cc; ++i)
  509.               childString += "<DD>"
  510.                              + NEWLINE
  511.                              + gatherDataHtml(bNode.getChild(i))
  512.                              + "</DD>"
  513.                              + NEWLINE;
  514.             bNode.containerOpen = wasOpen;
  515.             return childString + "</DL>" + NEWLINE;
  516.           }
  517.           if (self.nodeIsURI(bNode))
  518.             return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>" + NEWLINE;
  519.           if (self.nodeIsSeparator(bNode))
  520.             return "<HR>" + NEWLINE;
  521.           return "";
  522.         }
  523.         return gatherDataHtml(convertNode(aNode));
  524.     }
  525.     // case this.TYPE_UNICODE:
  526.     function gatherDataText(bNode) {
  527.       if (self.nodeIsLivemarkContainer(bNode))
  528.         return self.livemarks.getSiteURI(bNode.itemId).spec;
  529.       if (self.nodeIsContainer(bNode)) {
  530.         asContainer(bNode);
  531.         var wasOpen = bNode.containerOpen;
  532.         if (!wasOpen)
  533.           bNode.containerOpen = true;
  534.  
  535.         var childString = bNode.title + NEWLINE;
  536.         var cc = bNode.childCount;
  537.         for (var i = 0; i < cc; ++i) {
  538.           var child = bNode.getChild(i);
  539.           var suffix = i < (cc - 1) ? NEWLINE : "";
  540.           childString += gatherDataText(child) + suffix;
  541.         }
  542.         bNode.containerOpen = wasOpen;
  543.         return childString;
  544.       }
  545.       if (self.nodeIsURI(bNode))
  546.         return (aOverrideURI || bNode.uri);
  547.       if (self.nodeIsSeparator(bNode))
  548.         return "--------------------";
  549.       return "";
  550.     }
  551.  
  552.     return gatherDataText(convertNode(aNode));
  553.   },
  554.  
  555.   /**
  556.    * Unwraps data from the Clipboard or the current Drag Session.
  557.    * @param   blob
  558.    *          A blob (string) of data, in some format we potentially know how
  559.    *          to parse.
  560.    * @param   type
  561.    *          The content type of the blob.
  562.    * @returns An array of objects representing each item contained by the source.
  563.    */
  564.   unwrapNodes: function PU_unwrapNodes(blob, type) {
  565.     // We split on "\n"  because the transferable system converts "\r\n" to "\n"
  566.     var nodes = [];
  567.     switch(type) {
  568.       case this.TYPE_X_MOZ_PLACE:
  569.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  570.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  571.         var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  572.         nodes = JSON.decode("[" + blob + "]");
  573.         break;
  574.       case this.TYPE_X_MOZ_URL:
  575.         var parts = blob.split("\n");
  576.         // data in this type has 2 parts per entry, so if there are fewer
  577.         // than 2 parts left, the blob is malformed and we should stop
  578.         // but drag and drop of files from the shell has parts.length = 1
  579.         if (parts.length != 1 && parts.length % 2)
  580.           break;
  581.         for (var i = 0; i < parts.length; i=i+2) {
  582.           var uriString = parts[i];
  583.           var titleString = "";
  584.           if (parts.length > i+1)
  585.             titleString = parts[i+1];
  586.           else {
  587.             // for drag and drop of files, try to use the leafName as title
  588.             try {
  589.               titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
  590.                               .fileName;
  591.             }
  592.             catch (e) {}
  593.           }
  594.           // note:  this._uri() will throw if uriString is not a valid URI
  595.           if (this._uri(uriString)) {
  596.             nodes.push({ uri: uriString,
  597.                          title: titleString ? titleString : uriString ,
  598.                          type: this.TYPE_X_MOZ_URL });
  599.           }
  600.         }
  601.         break;
  602.       case this.TYPE_UNICODE:
  603.         var parts = blob.split("\n");
  604.         for (var i = 0; i < parts.length; i++) {
  605.           var uriString = parts[i];
  606.           // note: this._uri() will throw if uriString is not a valid URI
  607.           if (uriString != "" && this._uri(uriString))
  608.             nodes.push({ uri: uriString,
  609.                          title: uriString,
  610.                          type: this.TYPE_X_MOZ_URL });
  611.         }
  612.         break;
  613.       default:
  614.         LOG("Cannot unwrap data of type " + type);
  615.         throw Cr.NS_ERROR_INVALID_ARG;
  616.     }
  617.     return nodes;
  618.   },
  619.  
  620.   /**
  621.    * Generates a nsINavHistoryResult for the contents of a folder.
  622.    * @param   folderId
  623.    *          The folder to open
  624.    * @param   [optional] excludeItems
  625.    *          True to hide all items (individual bookmarks). This is used on
  626.    *          the left places pane so you just get a folder hierarchy.
  627.    * @param   [optional] expandQueries
  628.    *          True to make query items expand as new containers. For managing,
  629.    *          you want this to be false, for menus and such, you want this to
  630.    *          be true.
  631.    * @returns A nsINavHistoryResult containing the contents of the
  632.    *          folder. The result.root is guaranteed to be open.
  633.    */
  634.   getFolderContents:
  635.   function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
  636.     var query = this.history.getNewQuery();
  637.     query.setFolders([aFolderId], 1);
  638.     var options = this.history.getNewQueryOptions();
  639.     options.excludeItems = aExcludeItems;
  640.     options.expandQueries = aExpandQueries;
  641.  
  642.     var result = this.history.executeQuery(query, options);
  643.     result.root.containerOpen = true;
  644.     return result;
  645.   },
  646.  
  647.   /**
  648.    * Fetch all annotations for a URI, including all properties of each
  649.    * annotation which would be required to recreate it.
  650.    * @param aURI
  651.    *        The URI for which annotations are to be retrieved.
  652.    * @return Array of objects, each containing the following properties:
  653.    *         name, flags, expires, mimeType, type, value
  654.    */
  655.   getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
  656.     var annosvc = this.annotations;
  657.     var annos = [], val = null;
  658.     var annoNames = annosvc.getPageAnnotationNames(aURI, {});
  659.     for (var i = 0; i < annoNames.length; i++) {
  660.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  661.       annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, mimeType, storageType);
  662.       if (storageType.value == annosvc.TYPE_BINARY) {
  663.         var data = {}, length = {}, mimeType = {};
  664.         annosvc.getPageAnnotationBinary(aURI, annoNames[i], data, length, mimeType);
  665.         val = data.value;
  666.       }
  667.       else
  668.         val = annosvc.getPageAnnotation(aURI, annoNames[i]);
  669.  
  670.       annos.push({name: annoNames[i],
  671.                   flags: flags.value,
  672.                   expires: exp.value,
  673.                   mimeType: mimeType.value,
  674.                   type: storageType.value,
  675.                   value: val});
  676.     }
  677.     return annos;
  678.   },
  679.  
  680.   /**
  681.    * Fetch all annotations for an item, including all properties of each
  682.    * annotation which would be required to recreate it.
  683.    * @param aItemId
  684.    *        The identifier of the itme for which annotations are to be
  685.    *        retrieved.
  686.    * @return Array of objects, each containing the following properties:
  687.    *         name, flags, expires, mimeType, type, value
  688.    */
  689.   getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
  690.     var annosvc = this.annotations;
  691.     var annos = [], val = null;
  692.     var annoNames = annosvc.getItemAnnotationNames(aItemId, {});
  693.     for (var i = 0; i < annoNames.length; i++) {
  694.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  695.       annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, mimeType, storageType);
  696.       if (storageType.value == annosvc.TYPE_BINARY) {
  697.         var data = {}, length = {}, mimeType = {};
  698.         annosvc.geItemAnnotationBinary(aItemId, annoNames[i], data, length, mimeType);
  699.         val = data.value;
  700.       }
  701.       else
  702.         val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
  703.  
  704.       annos.push({name: annoNames[i],
  705.                   flags: flags.value,
  706.                   expires: exp.value,
  707.                   mimeType: mimeType.value,
  708.                   type: storageType.value,
  709.                   value: val});
  710.     }
  711.     return annos;
  712.   },
  713.  
  714.   /**
  715.    * Annotate a URI with a batch of annotations.
  716.    * @param aURI
  717.    *        The URI for which annotations are to be set.
  718.    * @param aAnnotations
  719.    *        Array of objects, each containing the following properties:
  720.    *        name, flags, expires, type, mimeType (only used for binary
  721.    *        annotations) value.
  722.    */
  723.   setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
  724.     var annosvc = this.annotations;
  725.     aAnnos.forEach(function(anno) {
  726.       var flags = ("flags" in anno) ? anno.flags : 0;
  727.       var expires = ("expires" in anno) ?
  728.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  729.       if (anno.type == annosvc.TYPE_BINARY) {
  730.         annosvc.setPageAnnotationBinary(aURI, anno.name, anno.value,
  731.                                         anno.value.length, anno.mimeType,
  732.                                         flags, expires);
  733.       }
  734.       else
  735.         annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
  736.     });
  737.   },
  738.  
  739.   /**
  740.    * Annotate an item with a batch of annotations.
  741.    * @param aItemId
  742.    *        The identifier of the item for which annotations are to be set
  743.    * @param aAnnotations
  744.    *        Array of objects, each containing the following properties:
  745.    *        name, flags, expires, type, mimeType (only used for binary
  746.    *        annotations) value.
  747.    */
  748.   setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
  749.     var annosvc = this.annotations;
  750.     aAnnos.forEach(function(anno) {
  751.       var flags = ("flags" in anno) ? anno.flags : 0;
  752.       var expires = ("expires" in anno) ?
  753.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  754.       if (anno.type == annosvc.TYPE_BINARY) {
  755.         annosvc.setItemAnnotationBinary(aItemId, anno.name, anno.value,
  756.                                         anno.value.length, anno.mimeType,
  757.                                         flags, expires);
  758.       }
  759.       else {
  760.         annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
  761.                                   expires);
  762.       }
  763.     });
  764.   },
  765.  
  766.   /**
  767.    * Helper for getting a serialized Places query for a particular folder.
  768.    * @param aFolderId The folder id to get a query for.
  769.    * @return string serialized place URI
  770.    */
  771.   getQueryStringForFolder: function PU_getQueryStringForFolder(aFolderId) {
  772.     var options = this.history.getNewQueryOptions();
  773.     var query = this.history.getNewQuery();
  774.     query.setFolders([aFolderId], 1);
  775.     return this.history.queriesToQueryString([query], 1, options);
  776.   },
  777.  
  778.   // identifier getters for special folders
  779.   get placesRootId() {
  780.     delete this.placesRootId;
  781.     return this.placesRootId = this.bookmarks.placesRoot;
  782.   },
  783.  
  784.   get bookmarksMenuFolderId() {
  785.     delete this.bookmarksMenuFolderId;
  786.     return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
  787.   },
  788.  
  789.   get toolbarFolderId() {
  790.     delete this.toolbarFolderId;
  791.     return this.toolbarFolderId = this.bookmarks.toolbarFolder;
  792.   },
  793.  
  794.   get tagsFolderId() {
  795.     delete this.tagsFolderId;
  796.     return this.tagsFolderId = this.bookmarks.tagsFolder;
  797.   },
  798.  
  799.   get unfiledBookmarksFolderId() {
  800.     delete this.unfiledBookmarksFolderId;
  801.     return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
  802.   },
  803.  
  804.   /**
  805.    * Set the POST data associated with a bookmark, if any.
  806.    * Used by POST keywords.
  807.    *   @param aBookmarkId
  808.    *   @returns string of POST data
  809.    */
  810.   setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
  811.     const annos = this.annotations;
  812.     if (aPostData)
  813.       annos.setItemAnnotation(aBookmarkId, POST_DATA_ANNO, aPostData, 
  814.                               0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  815.     else if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  816.       annos.removeItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  817.   },
  818.  
  819.   /**
  820.    * Get the POST data associated with a bookmark, if any.
  821.    * @param aBookmarkId
  822.    * @returns string of POST data if set for aBookmarkId. null otherwise.
  823.    */
  824.   getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
  825.     const annos = this.annotations;
  826.     if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  827.       return annos.getItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  828.  
  829.     return null;
  830.   },
  831.  
  832.   /**
  833.    * Get the URI (and any associated POST data) for a given keyword.
  834.    * @param aKeyword string keyword
  835.    * @returns an array containing a string URL and a string of POST data
  836.    */
  837.   getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
  838.     var url = null, postdata = null;
  839.     try {
  840.       var uri = this.bookmarks.getURIForKeyword(aKeyword);
  841.       if (uri) {
  842.         url = uri.spec;
  843.         var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri, {});
  844.         for (let i = 0; i < bookmarks.length; i++) {
  845.           var bookmark = bookmarks[i];
  846.           var kw = this.bookmarks.getKeywordForBookmark(bookmark);
  847.           if (kw == aKeyword) {
  848.             postdata = this.getPostDataForBookmark(bookmark);
  849.             break;
  850.           }
  851.         }
  852.       }
  853.     } catch(ex) {}
  854.     return [url, postdata];
  855.   },
  856.  
  857.   /**
  858.    * Get all bookmarks for a URL, excluding items under tag or livemark
  859.    * containers.
  860.    */
  861.   getBookmarksForURI:
  862.   function PU_getBookmarksForURI(aURI) {
  863.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  864.  
  865.     // filter the ids list
  866.     return bmkIds.filter(function(aID) {
  867.       var parent = this.bookmarks.getFolderIdForItem(aID);
  868.       // Livemark child
  869.       if (this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  870.         return false;
  871.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  872.       // item under a tag container
  873.       if (grandparent == this.tagsFolderId)
  874.         return false;
  875.       return true;
  876.     }, this);
  877.   },
  878.  
  879.   /**
  880.    * Get the most recently added/modified bookmark for a URL, excluding items
  881.    * under tag or livemark containers. -1 is returned if no item is found.
  882.    */
  883.   getMostRecentBookmarkForURI:
  884.   function PU_getMostRecentBookmarkForURI(aURI) {
  885.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  886.     for (var i = 0; i < bmkIds.length; i++) {
  887.       // Find the first folder which isn't a tag container
  888.       var bk = bmkIds[i];
  889.       var parent = this.bookmarks.getFolderIdForItem(bk);
  890.       if (parent == this.unfiledBookmarksFolderId)
  891.         return bk;
  892.  
  893.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  894.       if (grandparent != this.tagsFolderId &&
  895.           !this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  896.         return bk;
  897.     }
  898.     return -1;
  899.   },
  900.  
  901.   getMostRecentFolderForFeedURI:
  902.   function PU_getMostRecentFolderForFeedURI(aURI) {
  903.     var feedSpec = aURI.spec
  904.     var annosvc = this.annotations;
  905.     var livemarks = annosvc.getItemsWithAnnotation(LMANNO_FEEDURI, {});
  906.     for (var i = 0; i < livemarks.length; i++) {
  907.       if (annosvc.getItemAnnotation(livemarks[i], LMANNO_FEEDURI) == feedSpec)
  908.         return livemarks[i];
  909.     }
  910.     return -1;
  911.   },
  912.  
  913.   // Returns true if a container has uris in its first level
  914.   // Has better performances than checking getURLsForContainerNode(node).length
  915.   hasChildURIs: function PU_hasChildURIs(aNode) {
  916.     if (!this.nodeIsContainer(aNode))
  917.       return false;
  918.  
  919.     // in the Library left pane we use excludeItems
  920.     if (this.nodeIsFolder(aNode) && asQuery(aNode).queryOptions.excludeItems) {
  921.       var itemId = PlacesUtils.getConcreteItemId(aNode);
  922.       var contents = this.getFolderContents(itemId, false, false).root;
  923.       for (var i = 0; i < contents.childCount; ++i) {
  924.         var child = contents.getChild(i);
  925.         if (this.nodeIsURI(child))
  926.           return true;
  927.       }
  928.       return false;
  929.     }
  930.  
  931.     var wasOpen = aNode.containerOpen;
  932.     if (!wasOpen)
  933.       aNode.containerOpen = true;
  934.     var found = false;
  935.     for (var i = 0; i < aNode.childCount && !found; i++) {
  936.       var child = aNode.getChild(i);
  937.       if (this.nodeIsURI(child))
  938.         found = true;
  939.     }
  940.     if (!wasOpen)
  941.       aNode.containerOpen = false;
  942.     return found;
  943.   },
  944.  
  945.   getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
  946.     let urls = [];
  947.     if (this.nodeIsFolder(aNode) && asQuery(aNode).queryOptions.excludeItems) {
  948.       // grab manually
  949.       var itemId = this.getConcreteItemId(aNode);
  950.       let contents = this.getFolderContents(itemId, false, false).root;
  951.       for (let i = 0; i < contents.childCount; ++i) {
  952.         let child = contents.getChild(i);
  953.         if (this.nodeIsURI(child))
  954.           urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
  955.       }
  956.     }
  957.     else {
  958.       let result, oldViewer, wasOpen;
  959.       try {
  960.         let wasOpen = aNode.containerOpen;
  961.         result = aNode.parentResult;
  962.         oldViewer = result.viewer;
  963.         if (!wasOpen) {
  964.           result.viewer = null;
  965.           aNode.containerOpen = true;
  966.         }
  967.         for (let i = 0; i < aNode.childCount; ++i) {
  968.           // Include visible url nodes only
  969.           let child = aNode.getChild(i);
  970.           if (this.nodeIsURI(child)) {
  971.             // If the node contents is visible, add the uri only if its node is
  972.             // visible. Otherwise follow viewer's collapseDuplicates property,
  973.             // default to true
  974.             if ((wasOpen && oldViewer && child.viewIndex != -1) ||
  975.                 (oldViewer && !oldViewer.collapseDuplicates) ||
  976.                 urls.indexOf(child.uri) == -1) {
  977.               urls.push({ uri: child.uri,
  978.                           isBookmark: this.nodeIsBookmark(child) });
  979.             }
  980.           }
  981.         }
  982.         if (!wasOpen)
  983.           aNode.containerOpen = false;
  984.       }
  985.       finally {
  986.         if (!wasOpen)
  987.           result.viewer = oldViewer;
  988.       }
  989.     }
  990.  
  991.     return urls;
  992.   },
  993.  
  994.   /**
  995.    * Restores bookmarks/tags from a JSON file.
  996.    * WARNING: This method *removes* any bookmarks in the collection before
  997.    * restoring from the file.
  998.    *
  999.    * @param aFile
  1000.    *        nsIFile of bookmarks in JSON format to be restored.
  1001.    * @param aExcludeItems
  1002.    *        Array of root item ids (ie: children of the places root)
  1003.    *        to not delete when restoring.
  1004.    */
  1005.   restoreBookmarksFromJSONFile:
  1006.   function PU_restoreBookmarksFromJSONFile(aFile, aExcludeItems) {
  1007.     // open file stream
  1008.     var stream = Cc["@mozilla.org/network/file-input-stream;1"].
  1009.                  createInstance(Ci.nsIFileInputStream);
  1010.     stream.init(aFile, 0x01, 0, 0);
  1011.     var converted = Cc["@mozilla.org/intl/converter-input-stream;1"].
  1012.                     createInstance(Ci.nsIConverterInputStream);
  1013.     converted.init(stream, "UTF-8", 1024,
  1014.                    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  1015.  
  1016.     // read in contents
  1017.     var str = {};
  1018.     var jsonStr = "";
  1019.     while (converted.readString(4096, str) != 0)
  1020.       jsonStr += str.value;
  1021.     converted.close();
  1022.  
  1023.     if (jsonStr.length == 0)
  1024.       return; // empty file
  1025.  
  1026.     this.restoreBookmarksFromJSONString(jsonStr, true, aExcludeItems);
  1027.   },
  1028.  
  1029.   /**
  1030.    * Import bookmarks from a JSON string.
  1031.    * 
  1032.    * @param aString
  1033.    *        JSON string of serialized bookmark data.
  1034.    * @param aReplace
  1035.    *        Boolean if true, replace existing bookmarks, else merge.
  1036.    * @param aExcludeItems
  1037.    *        Array of root item ids (ie: children of the places root)
  1038.    *        to not delete when restoring.
  1039.    */
  1040.   restoreBookmarksFromJSONString:
  1041.   function PU_restoreBookmarksFromJSONString(aString, aReplace, aExcludeItems) {
  1042.     // convert string to JSON
  1043.     var nodes = this.unwrapNodes(aString, this.TYPE_X_MOZ_PLACE_CONTAINER);
  1044.  
  1045.     if (nodes.length == 0 || !nodes[0].children ||
  1046.         nodes[0].children.length == 0)
  1047.       return; // nothing to restore
  1048.  
  1049.     // ensure tag folder gets processed last
  1050.     nodes[0].children.sort(function sortRoots(aNode, bNode) {
  1051.       return (aNode.root && aNode.root == "tagsFolder") ? 1 :
  1052.               (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
  1053.     });
  1054.  
  1055.     var batch = {
  1056.       _utils: this,
  1057.       nodes: nodes[0].children,
  1058.       runBatched: function restore_runBatched() {
  1059.         if (aReplace) {
  1060.           var excludeItems = aExcludeItems || [];
  1061.           // delete existing children of the root node, excepting:
  1062.           // 1. special folders: delete the child nodes 
  1063.           // 2. tags folder: untag via the tagging api
  1064.           var query = this._utils.history.getNewQuery();
  1065.           query.setFolders([this._utils.placesRootId], 1);
  1066.           var options = this._utils.history.getNewQueryOptions();
  1067.           options.expandQueries = false;
  1068.           var root = this._utils.history.executeQuery(query, options).root;
  1069.           root.containerOpen = true;
  1070.           var childIds = [];
  1071.           for (var i = 0; i < root.childCount; i++) {
  1072.             var childId = root.getChild(i).itemId;
  1073.             if (excludeItems.indexOf(childId) == -1)
  1074.               childIds.push(childId);
  1075.           }
  1076.           root.containerOpen = false;
  1077.  
  1078.           for (var i = 0; i < childIds.length; i++) {
  1079.             var rootItemId = childIds[i];
  1080.             if (rootItemId == this._utils.tagsFolderId) {
  1081.               // remove tags via the tagging service
  1082.               var tags = this._utils.tagging.allTags;
  1083.               var uris = [];
  1084.               var bogusTagContainer = false;
  1085.               for (let i in tags) {
  1086.                 var tagURIs = [];
  1087.                 // skip empty tags since getURIsForTag would throw
  1088.                 if (tags[i])
  1089.                   tagURIs = this._utils.tagging.getURIsForTag(tags[i]);
  1090.  
  1091.                 if (!tagURIs.length) {
  1092.                   // This is a bogus tag container, empty tags should be removed
  1093.                   // automatically, but this does not work if they contain some
  1094.                   // not-uri node, so we remove them manually.
  1095.                   // XXX this is a temporary workaround until we implement
  1096.                   // preventive database maintainance in bug 431558.
  1097.                   bogusTagContainer = true;
  1098.                 }
  1099.                 for (let j in tagURIs)
  1100.                   this._utils.tagging.untagURI(tagURIs[j], [tags[i]]);
  1101.               }
  1102.               if (bogusTagContainer)
  1103.                 this._utils.bookmarks.removeFolderChildren(rootItemId);
  1104.             }
  1105.             else if ([this._utils.toolbarFolderId,
  1106.                       this._utils.unfiledBookmarksFolderId,
  1107.                       this._utils.bookmarksMenuFolderId].indexOf(rootItemId) != -1)
  1108.               this._utils.bookmarks.removeFolderChildren(rootItemId);
  1109.             else
  1110.               this._utils.bookmarks.removeItem(rootItemId);
  1111.           }
  1112.         }
  1113.  
  1114.         var searchIds = [];
  1115.         var folderIdMap = [];
  1116.  
  1117.         this.nodes.forEach(function(node) {
  1118.           if (!node.children || node.children.length == 0)
  1119.             return; // nothing to restore for this root
  1120.  
  1121.           if (node.root) {
  1122.             var container = this.placesRootId; // default to places root
  1123.             switch (node.root) {
  1124.               case "bookmarksMenuFolder":
  1125.                 container = this.bookmarksMenuFolderId;
  1126.                 break;
  1127.               case "tagsFolder":
  1128.                 container = this.tagsFolderId;
  1129.                 break;
  1130.               case "unfiledBookmarksFolder":
  1131.                 container = this.unfiledBookmarksFolderId;
  1132.                 break;
  1133.               case "toolbarFolder":
  1134.                 container = this.toolbarFolderId;
  1135.                 break;
  1136.             }
  1137.  
  1138.             // insert the data into the db
  1139.             node.children.forEach(function(child) {
  1140.               var index = child.index;
  1141.               var [folders, searches] = this.importJSONNode(child, container, index);
  1142.               folderIdMap = folderIdMap.concat(folders);
  1143.               searchIds = searchIds.concat(searches);
  1144.             }, this);
  1145.           }
  1146.           else
  1147.             this.importJSONNode(node, this.placesRootId, node.index);
  1148.  
  1149.         }, this._utils);
  1150.  
  1151.         // fixup imported place: uris that contain folders
  1152.         searchIds.forEach(function(aId) {
  1153.           var oldURI = this.bookmarks.getBookmarkURI(aId);
  1154.           var uri = this._fixupQuery(this.bookmarks.getBookmarkURI(aId),
  1155.                                      folderIdMap);
  1156.           if (!uri.equals(oldURI))
  1157.             this.bookmarks.changeBookmarkURI(aId, uri);
  1158.         }, this._utils);
  1159.       }
  1160.     };
  1161.  
  1162.     this.bookmarks.runInBatchMode(batch, null);
  1163.   },
  1164.  
  1165.   /**
  1166.    * Takes a JSON-serialized node and inserts it into the db.
  1167.    *
  1168.    * @param   aData
  1169.    *          The unwrapped data blob of dropped or pasted data.
  1170.    * @param   aContainer
  1171.    *          The container the data was dropped or pasted into
  1172.    * @param   aIndex
  1173.    *          The index within the container the item was dropped or pasted at
  1174.    * @returns an array containing of maps of old folder ids to new folder ids,
  1175.    *          and an array of saved search ids that need to be fixed up.
  1176.    *          eg: [[[oldFolder1, newFolder1]], [search1]]
  1177.    */
  1178.   importJSONNode: function PU_importJSONNode(aData, aContainer, aIndex) {
  1179.     var folderIdMap = [];
  1180.     var searchIds = [];
  1181.     var id = -1;
  1182.     switch (aData.type) {
  1183.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  1184.         if (aContainer == PlacesUtils.bookmarks.tagsFolder) {
  1185.           if (aData.children) {
  1186.             aData.children.forEach(function(aChild) {
  1187.               try {
  1188.                 this.tagging.tagURI(this._uri(aChild.uri), [aData.title]);
  1189.               } catch (ex) {
  1190.                 // invalid tag child, skip it
  1191.               }
  1192.             }, this);
  1193.             return [folderIdMap, searchIds];
  1194.           }
  1195.         }
  1196.         else if (aData.livemark && aData.annos) {
  1197.           // node is a livemark
  1198.           var feedURI = null;
  1199.           var siteURI = null;
  1200.           aData.annos = aData.annos.filter(function(aAnno) {
  1201.             if (aAnno.name == LMANNO_FEEDURI) {
  1202.               feedURI = this._uri(aAnno.value);
  1203.               return false;
  1204.             }
  1205.             else if (aAnno.name == LMANNO_SITEURI) {
  1206.               siteURI = this._uri(aAnno.value);
  1207.               return false;
  1208.             }
  1209.             return true;
  1210.           }, this);
  1211.  
  1212.           if (feedURI)
  1213.             id = this.livemarks.createLivemark(aContainer, aData.title, siteURI, feedURI, aIndex);
  1214.         }
  1215.         else {
  1216.           id = this.bookmarks.createFolder(aContainer, aData.title, aIndex);
  1217.           folderIdMap.push([aData.id, id]);
  1218.           // process children
  1219.           if (aData.children) {
  1220.             aData.children.every(function(aChild, aIndex) {
  1221.               var [folderIds, searches] = this.importJSONNode(aChild, id, aIndex);
  1222.               folderIdMap = folderIdMap.concat(folderIds);
  1223.               searchIds = searchIds.concat(searches);
  1224.               return true;
  1225.             }, this);
  1226.           }
  1227.         }
  1228.         break;
  1229.       case this.TYPE_X_MOZ_PLACE:
  1230.         id = this.bookmarks.insertBookmark(aContainer, this._uri(aData.uri), aIndex, aData.title);
  1231.         if (aData.keyword)
  1232.           this.bookmarks.setKeywordForBookmark(id, aData.keyword);
  1233.         if (aData.tags) {
  1234.           var tags = aData.tags.split(", ");
  1235.           if (tags.length)
  1236.             this.tagging.tagURI(this._uri(aData.uri), tags);
  1237.         }
  1238.         if (aData.charset)
  1239.           this.history.setCharsetForURI(this._uri(aData.uri), aData.charset);
  1240.         if (aData.uri.match(/^place:/))
  1241.           searchIds.push(id);
  1242.         break;
  1243.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  1244.         id = this.bookmarks.insertSeparator(aContainer, aIndex);
  1245.         break;
  1246.       default:
  1247.     }
  1248.  
  1249.     // set generic properties
  1250.     if (id != -1) {
  1251.       this.bookmarks.setItemDateAdded(id, aData.dateAdded);
  1252.       this.bookmarks.setItemLastModified(id, aData.lastModified);
  1253.       if (aData.annos)
  1254.         this.setAnnotationsForItem(id, aData.annos);
  1255.     }
  1256.  
  1257.     return [folderIdMap, searchIds];
  1258.   },
  1259.  
  1260.   /**
  1261.    * Replaces imported folder ids with their local counterparts in a place: URI.
  1262.    *
  1263.    * @param   aURI
  1264.    *          A place: URI with folder ids.
  1265.    * @param   aFolderIdMap
  1266.    *          An array mapping old folder id to new folder ids.
  1267.    * @returns the fixed up URI if all matched. If some matched, it returns
  1268.    *          the URI with only the matching folders included. If none matched it
  1269.    *          returns the input URI unchanged.
  1270.    */
  1271.   _fixupQuery: function PU__fixupQuery(aQueryURI, aFolderIdMap) {
  1272.     var queries = {};
  1273.     var options = {};
  1274.     this.history.queryStringToQueries(aQueryURI.spec, queries, {}, options);
  1275.  
  1276.     var fixedQueries = [];
  1277.     queries.value.forEach(function(aQuery) {
  1278.       var folders = aQuery.getFolders({});
  1279.  
  1280.       var newFolders = [];
  1281.       aFolderIdMap.forEach(function(aMapping) {
  1282.         if (folders.indexOf(aMapping[0]) != -1)
  1283.           newFolders.push(aMapping[1]);
  1284.       });
  1285.  
  1286.       if (newFolders.length)
  1287.         aQuery.setFolders(newFolders, newFolders.length);
  1288.       fixedQueries.push(aQuery);
  1289.     });
  1290.  
  1291.     var stringURI = this.history.queriesToQueryString(fixedQueries,
  1292.                                                       fixedQueries.length,
  1293.                                                       options.value);
  1294.     return this._uri(stringURI);
  1295.   },
  1296.  
  1297.   /**
  1298.    * Serializes the given node (and all it's descendents) as JSON
  1299.    * and writes the serialization to the given output stream.
  1300.    * 
  1301.    * @param   aNode
  1302.    *          An nsINavHistoryResultNode
  1303.    * @param   aStream
  1304.    *          An nsIOutputStream. NOTE: it only uses the write(str, len)
  1305.    *          method of nsIOutputStream. The caller is responsible for
  1306.    *          closing the stream.
  1307.    * @param   aIsUICommand
  1308.    *          Boolean - If true, modifies serialization so that each node self-contained.
  1309.    *          For Example, tags are serialized inline with each bookmark.
  1310.    * @param   aResolveShortcuts
  1311.    *          Converts folder shortcuts into actual folders. 
  1312.    * @param   aExcludeItems
  1313.    *          An array of item ids that should not be written to the backup.
  1314.    */
  1315.   serializeNodeAsJSONToOutputStream:
  1316.   function PU_serializeNodeAsJSONToOutputStream(aNode, aStream, aIsUICommand,
  1317.                                                 aResolveShortcuts,
  1318.                                                 aExcludeItems) {
  1319.     var self = this;
  1320.     
  1321.     function addGenericProperties(aPlacesNode, aJSNode) {
  1322.       aJSNode.title = aPlacesNode.title;
  1323.       var id = aPlacesNode.itemId;
  1324.       if (id != -1) {
  1325.         aJSNode.id = id;
  1326.  
  1327.         var parent = aPlacesNode.parent;
  1328.         if (parent)
  1329.           aJSNode.parent = parent.itemId;
  1330.         var dateAdded = aPlacesNode.dateAdded;
  1331.         if (dateAdded)
  1332.           aJSNode.dateAdded = dateAdded;
  1333.         var lastModified = aPlacesNode.lastModified;
  1334.         if (lastModified)
  1335.           aJSNode.lastModified = lastModified;
  1336.  
  1337.         // XXX need a hasAnnos api
  1338.         var annos = [];
  1339.         try {
  1340.           annos = self.getAnnotationsForItem(id).filter(function(anno) {
  1341.             // XXX should whitelist this instead, w/ a pref for
  1342.             // backup/restore of non-whitelisted annos
  1343.             // XXX causes JSON encoding errors, so utf-8 encode
  1344.             //anno.value = unescape(encodeURIComponent(anno.value));
  1345.             if (anno.name == LMANNO_FEEDURI)
  1346.               aJSNode.livemark = 1;
  1347.             if (anno.name == READ_ONLY_ANNO && aResolveShortcuts) {
  1348.               // When copying a read-only node, remove the read-only annotation.
  1349.               return false;
  1350.             }
  1351.             return true;
  1352.           });
  1353.         } catch(ex) {
  1354.           LOG(ex);
  1355.         }
  1356.         if (annos.length != 0)
  1357.           aJSNode.annos = annos;
  1358.       }
  1359.       // XXXdietrich - store annos for non-bookmark items
  1360.     }
  1361.  
  1362.     function addURIProperties(aPlacesNode, aJSNode) {
  1363.       aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1364.       aJSNode.uri = aPlacesNode.uri;
  1365.       if (aJSNode.id && aJSNode.id != -1) {
  1366.         // harvest bookmark-specific properties
  1367.         var keyword = self.bookmarks.getKeywordForBookmark(aJSNode.id);
  1368.         if (keyword)
  1369.           aJSNode.keyword = keyword;
  1370.       }
  1371.  
  1372.       var tags = aIsUICommand ? aPlacesNode.tags : null;
  1373.       if (tags)
  1374.         aJSNode.tags = tags;
  1375.  
  1376.       // last character-set
  1377.       var uri = self._uri(aPlacesNode.uri);
  1378.       var lastCharset = self.history.getCharsetForURI(uri);
  1379.       if (lastCharset)
  1380.         aJSNode.charset = lastCharset;
  1381.     }
  1382.  
  1383.     function addSeparatorProperties(aPlacesNode, aJSNode) {
  1384.       aJSNode.type = self.TYPE_X_MOZ_PLACE_SEPARATOR;
  1385.     }
  1386.  
  1387.     function addContainerProperties(aPlacesNode, aJSNode) {
  1388.       // saved queries
  1389.       var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
  1390.       if (aJSNode.id != -1 && (PlacesUtils.nodeIsQuery(aPlacesNode) ||
  1391.           (concreteId != aPlacesNode.itemId && !aResolveShortcuts))) {
  1392.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1393.         aJSNode.uri = aPlacesNode.uri;
  1394.         // folder shortcut
  1395.         if (aIsUICommand)
  1396.           aJSNode.concreteId = concreteId;
  1397.         return;
  1398.       }
  1399.       else if (aJSNode.id != -1) { // bookmark folder
  1400.         if (concreteId != aPlacesNode.itemId)
  1401.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1402.         aJSNode.type = self.TYPE_X_MOZ_PLACE_CONTAINER;
  1403.         // mark special folders
  1404.         if (aJSNode.id == self.bookmarks.placesRoot)
  1405.           aJSNode.root = "placesRoot";
  1406.         else if (aJSNode.id == self.bookmarks.bookmarksMenuFolder)
  1407.           aJSNode.root = "bookmarksMenuFolder";
  1408.         else if (aJSNode.id == self.bookmarks.tagsFolder)
  1409.           aJSNode.root = "tagsFolder";
  1410.         else if (aJSNode.id == self.bookmarks.unfiledBookmarksFolder)
  1411.           aJSNode.root = "unfiledBookmarksFolder";
  1412.         else if (aJSNode.id == self.bookmarks.toolbarFolder)
  1413.           aJSNode.root = "toolbarFolder";
  1414.       }
  1415.     }
  1416.  
  1417.     function writeScalarNode(aStream, aNode) {
  1418.       // serialize to json
  1419.       var jstr = self.toJSONString(aNode);
  1420.       // write to stream
  1421.       aStream.write(jstr, jstr.length);
  1422.     }
  1423.  
  1424.     function writeComplexNode(aStream, aNode, aSourceNode) {
  1425.       var escJSONStringRegExp = /(["\\])/g;
  1426.       // write prefix
  1427.       var properties = [];
  1428.       for (let [name, value] in Iterator(aNode)) {
  1429.         if (name == "annos")
  1430.           value = self.toJSONString(value);
  1431.         else if (typeof value == "string")
  1432.           value = "\"" + value.replace(escJSONStringRegExp, '\\$1') + "\"";
  1433.         properties.push("\"" + name.replace(escJSONStringRegExp, '\\$1') + "\":" + value);
  1434.       }
  1435.       var jStr = "{" + properties.join(",") + ",\"children\":[";
  1436.       aStream.write(jStr, jStr.length);
  1437.  
  1438.       // write child nodes
  1439.       if (!aNode.livemark) {
  1440.         asContainer(aSourceNode);
  1441.         var wasOpen = aSourceNode.containerOpen;
  1442.         if (!wasOpen)
  1443.           aSourceNode.containerOpen = true;
  1444.         var cc = aSourceNode.childCount;
  1445.         for (var i = 0; i < cc; ++i) {
  1446.           var childNode = aSourceNode.getChild(i);
  1447.           if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
  1448.             continue;
  1449.           var written = serializeNodeToJSONStream(aSourceNode.getChild(i), i);
  1450.           if (written && i < cc - 1)
  1451.             aStream.write(",", 1);
  1452.         }
  1453.         if (!wasOpen)
  1454.           aSourceNode.containerOpen = false;
  1455.       }
  1456.  
  1457.       // write suffix
  1458.       aStream.write("]}", 2);
  1459.     }
  1460.  
  1461.     function serializeNodeToJSONStream(bNode, aIndex) {
  1462.       var node = {};
  1463.  
  1464.       // set index in order received
  1465.       // XXX handy shortcut, but are there cases where we don't want
  1466.       // to export using the sorting provided by the query?
  1467.       if (aIndex)
  1468.         node.index = aIndex;
  1469.  
  1470.       addGenericProperties(bNode, node);
  1471.  
  1472.       var parent = bNode.parent;
  1473.       var grandParent = parent ? parent.parent : null;
  1474.  
  1475.       if (self.nodeIsURI(bNode)) {
  1476.         // Tag root accept only folder nodes
  1477.         if (parent && parent.itemId == self.tagsFolderId)
  1478.           return false;
  1479.         // Check for url validity, since we can't halt while writing a backup.
  1480.         // This will throw if we try to serialize an invalid url and it does
  1481.         // not make sense saving a wrong or corrupt uri node.
  1482.         try {
  1483.           self._uri(bNode.uri);
  1484.         } catch (ex) {
  1485.           return false;
  1486.         }
  1487.         addURIProperties(bNode, node);
  1488.       }
  1489.       else if (self.nodeIsContainer(bNode)) {
  1490.         // Tag containers accept only uri nodes
  1491.         if (grandParent && grandParent.itemId == self.tagsFolderId)
  1492.           return false;
  1493.         addContainerProperties(bNode, node);
  1494.       }
  1495.       else if (self.nodeIsSeparator(bNode)) {
  1496.         // Tag root accept only folder nodes
  1497.         // Tag containers accept only uri nodes
  1498.         if ((parent && parent.itemId == self.tagsFolderId) ||
  1499.             (grandParent && grandParent.itemId == self.tagsFolderId))
  1500.           return false;
  1501.  
  1502.         addSeparatorProperties(bNode, node);
  1503.       }
  1504.  
  1505.       if (!node.feedURI && node.type == self.TYPE_X_MOZ_PLACE_CONTAINER)
  1506.         writeComplexNode(aStream, node, bNode);
  1507.       else
  1508.         writeScalarNode(aStream, node);
  1509.       return true;
  1510.     }
  1511.  
  1512.     // serialize to stream
  1513.     serializeNodeToJSONStream(aNode, null);
  1514.   },
  1515.  
  1516.   /**
  1517.    * Serialize a JS object to JSON
  1518.    */
  1519.   toJSONString: function PU_toJSONString(aObj) {
  1520.     var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  1521.     return JSON.encode(aObj);
  1522.   },
  1523.  
  1524.   /**
  1525.    * Serializes bookmarks using JSON, and writes to the supplied file.
  1526.    */
  1527.   backupBookmarksToFile: function PU_backupBookmarksToFile(aFile, aExcludeItems) {
  1528.     if (aFile.exists() && !aFile.isWritable())
  1529.       return; // XXX
  1530.  
  1531.     // init stream
  1532.     var stream = Cc["@mozilla.org/network/file-output-stream;1"].
  1533.                  createInstance(Ci.nsIFileOutputStream);
  1534.     stream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
  1535.  
  1536.     // utf-8 converter stream
  1537.     var converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
  1538.                  createInstance(Ci.nsIConverterOutputStream);
  1539.     converter.init(stream, "UTF-8", 0, 0x0000);
  1540.  
  1541.     // weep over stream interface variance
  1542.     var streamProxy = {
  1543.       converter: converter,
  1544.       write: function(aData, aLen) {
  1545.         this.converter.writeString(aData);
  1546.       }
  1547.     };
  1548.  
  1549.     // query places root
  1550.     var options = this.history.getNewQueryOptions();
  1551.     options.expandQueries = false;
  1552.     var query = this.history.getNewQuery();
  1553.     query.setFolders([this.bookmarks.placesRoot], 1);
  1554.     var result = this.history.executeQuery(query, options);
  1555.     result.root.containerOpen = true;
  1556.     // serialize as JSON, write to stream
  1557.     this.serializeNodeAsJSONToOutputStream(result.root, streamProxy,
  1558.                                            false, false, aExcludeItems);
  1559.     result.root.containerOpen = false;
  1560.  
  1561.     // close converter and stream
  1562.     converter.close();
  1563.     stream.close();
  1564.   },
  1565.  
  1566.   /**
  1567.    * ArchiveBookmarksFile()
  1568.    *
  1569.    * Creates a dated backup once a day in <profile>/bookmarkbackups.
  1570.    * Stores the bookmarks using JSON.
  1571.    *
  1572.    * @param int aNumberOfBackups - the maximum number of backups to keep
  1573.    *
  1574.    * @param bool aForceArchive - forces creating an archive even if one was 
  1575.    *                             already created that day (overwrites)
  1576.    */
  1577.   archiveBookmarksFile:
  1578.   function PU_archiveBookmarksFile(aNumberOfBackups, aForceArchive) {
  1579.     // get/create backups directory
  1580.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1581.                      getService(Ci.nsIProperties);
  1582.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1583.     bookmarksBackupDir.append("bookmarkbackups");
  1584.     if (!bookmarksBackupDir.exists()) {
  1585.       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
  1586.       if (!bookmarksBackupDir.exists())
  1587.         return; // unable to create directory!
  1588.     }
  1589.  
  1590.     // construct the new leafname
  1591.     // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
  1592.     // and makes the alphabetical order of multiple backup files more useful.
  1593.     var date = new Date().toLocaleFormat("%Y-%m-%d");
  1594.     var backupFilename = this.getFormattedString("bookmarksArchiveFilename", [date]);
  1595.  
  1596.     var backupFile = null;
  1597.     if (!aForceArchive) {
  1598.       var backupFileNames = [];
  1599.       var backupFilenamePrefix = backupFilename.substr(0, backupFilename.indexOf("-"));
  1600.       var entries = bookmarksBackupDir.directoryEntries;
  1601.       while (entries.hasMoreElements()) {
  1602.         var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1603.         var backupName = entry.leafName;
  1604.         if (backupName.substr(0, backupFilenamePrefix.length) == backupFilenamePrefix) {
  1605.           if (backupName == backupFilename)
  1606.             backupFile = entry;
  1607.           backupFileNames.push(backupName);
  1608.         }
  1609.       }
  1610.  
  1611.       var numberOfBackupsToDelete = 0;
  1612.       if (aNumberOfBackups > -1)
  1613.         numberOfBackupsToDelete = backupFileNames.length - aNumberOfBackups;
  1614.  
  1615.       if (numberOfBackupsToDelete > 0) {
  1616.         // If we don't have today's backup, remove one more so that
  1617.         // the total backups after this operation does not exceed the
  1618.         // number specified in the pref.
  1619.         if (!backupFile)
  1620.           numberOfBackupsToDelete++;
  1621.  
  1622.         backupFileNames.sort();
  1623.         while (numberOfBackupsToDelete--) {
  1624.           let backupFile = bookmarksBackupDir.clone();
  1625.           backupFile.append(backupFileNames[0]);
  1626.           backupFile.remove(false);
  1627.           backupFileNames.shift();
  1628.         }
  1629.       }
  1630.  
  1631.       // do nothing if we either have today's backup already
  1632.       // or the user has set the pref to zero.
  1633.       if (backupFile || aNumberOfBackups == 0)
  1634.         return;
  1635.     }
  1636.  
  1637.     backupFile = bookmarksBackupDir.clone();
  1638.     backupFile.append(backupFilename);
  1639.  
  1640.     if (aForceArchive && backupFile.exists())
  1641.         backupFile.remove(false);
  1642.  
  1643.     if (!backupFile.exists())
  1644.       backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
  1645.  
  1646.     this.backupBookmarksToFile(backupFile);
  1647.   },
  1648.  
  1649.   /**
  1650.    * Get the most recent backup file.
  1651.    * @returns nsIFile backup file
  1652.    */
  1653.   getMostRecentBackup: function PU_getMostRecentBackup() {
  1654.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1655.                      getService(Ci.nsIProperties);
  1656.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1657.     bookmarksBackupDir.append("bookmarkbackups");
  1658.     if (!bookmarksBackupDir.exists())
  1659.       return null;
  1660.  
  1661.     var backups = [];
  1662.     var entries = bookmarksBackupDir.directoryEntries;
  1663.     while (entries.hasMoreElements()) {
  1664.       var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1665.       if (!entry.isHidden() && entry.leafName.match(/^bookmarks-.+(html|json)?$/))
  1666.         backups.push(entry.leafName);
  1667.     }
  1668.  
  1669.     if (backups.length ==  0)
  1670.       return null;
  1671.  
  1672.     backups.sort();
  1673.     var filename = backups.pop();
  1674.  
  1675.     var backupFile = bookmarksBackupDir.clone();
  1676.     backupFile.append(filename);
  1677.     return backupFile;
  1678.   }
  1679. };
  1680.